package Client;

import Share.*;

import java.io.*;
import java.security.SignatureException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class Client {
	private ConcurrentHashMap<Integer, Token> tokens = new ConcurrentHashMap<Integer, Token>();
	private HashMap<String, Command> commands = new HashMap<String, Command>();
	private static String newLine = System.getProperty("line.separator");
	private TransmitionBox com = new TransmitionBox();
	
	private AtomicInteger nextTokenNumber = new AtomicInteger(1);
	
	public static final int MAX_PORT_NUMBER = 65535;
	
	private static final int DEFAULT_SERVER_PORT = 4040;
	private static final int DEFAULT_CA_PORT = 5050;
	private static final int DEFAULT_CLIENT_PORT = 6060;
	
	private static final int DEFAULT_DELEGATE_TOKEN_WINDOW = 3600000; //One hour in milliseconds;
	private static final int SHORT_TOKEN_WINDOW = 600000; // 10 minutes in milliseconds
	
	public Boolean running = true;
	
	private abstract class Command
	{
		private String name;
		private String help;
		private String usage;
		
		public Command(String name, String usage, String help)
		{
			this.name = name;
			this.usage = usage;
			this.help = help;
		}
		public String getName()
		{
			return name;
		}
		public void printHelp()
		{
			if(usage != null)
			{
				System.out.format("Usage:%1$s ",name);
				System.out.println(usage);
			}
			System.out.println(help);
		}
		protected void printStatus(TransmitStatus status)
		{
			PrintStream out = System.out;
			switch(status)
			{
			case clientFileNotFound:
				out.println("Could not find file on local machine.");
				break;
			case clientFileReceiveError:
				out.println("There was an error recieveing the file.");
				break;
			case clientFileSendError:
				out.println("There was an error sending the file.");
				break;
			case clientWriteError:
				out.println("Unable to write to local file.");
				break;
			case error:
				out.println("There was an unspecified error.");
				break;
			case hostNotFound:
				out.println("The host was not founds on the specified server or port");
				break;
			case insufficentPermission:
				out.println("Insufficent Permission so access the speciffied server file.");
				break;
			case serverFileNotFound:
				out.println("There server could not find the file.");
				break;
			case serverFileSendError:
				out.println("There server had and error sending the file.");
				break;
			case serverWriteError:
				out.println("The server had an error writing the file.");
				break;
			case success:
				out.println("Success.");
				break;
			case badHash:
				out.println("A bad hash was detected.");
				break;
			case invalidToken:
				out.println("An invalid token was recieved. Action is cancelled.");
				break;
			case certificateDenied:
				out.println("The certificate request was denied.");
				break;
			case invalidCertificate:
				out.println("Invalid certificate received.");
				break;
			case invalidName:
				out.println("The specified name is invalid");
				break;
			case noSession:
				out.println("There is no open session");
			case wrongToken:
				out.println("One of the session certificates did not match the token");
			}
		}
		public abstract void action(String[] args, TransmitionBox com);
	}
	
	private class ReceiveTokenHandle extends WaitRequestHandler
	{
		TransmitionBox box;
		public ReceiveTokenHandle(TransmitionBox box)
		{
			this.blocking = false;
			this.box = box;
		}
		@Override
		public Boolean connectionRecieved(Token token, SecureDataOutputStream out,
				SecureDataInputStream in) {
			try {
				if(!token.deleg.equals(box.getName()))
				{	
					out.write(TransmitStatus.wrongToken.ordinal());
					return false;
				}
				else
				{
					int tokenNumber = nextTokenNumber.getAndIncrement();
					tokens.put(tokenNumber, token);
				}
			
				out.write(TransmitStatus.success.ordinal());
			} catch (IOException e) {
				return false;
			}
			return false;
		}
		@Override
		public void handlerError(Exception error)
		{
			box.stop();
			if(blocking)
			{
				System.out.println("There was an error in the Wait thread the program will be closed.");
				System.exit(-1);
			}
			else
			{
				System.out.println("There was an error in the wait thread and it has been closed.");
			}
		}
	}
	
	public Client()
	{
		buildCommandList();
	}
	
	private Token createToken(String request, TokenType type, Boolean allowDelegate, Boolean delegate)
	{
		if(!request.startsWith("-t:"))
		{
			int fileNumber = 0;
			try
			{
				fileNumber = Integer.parseInt(request);
			}
			catch (NumberFormatException error)
			{
				System.out.println("Invalid file number!");
				return null;
			}
			
			Date expiration = new Date();
			expiration.setTime(expiration.getTime() + SHORT_TOKEN_WINDOW);
			
			try {
				return new Token(
						"server",
						com.getName(),
						com.getName(),
						fileNumber,
						type,
						allowDelegate, 
						expiration,
						com.getCertificate(),
						null);
			} catch (IOException e) {
				return null;
			}
		}
		else
		{
			request = request.substring(3);
			int tokenNumber = 0;
			try
			{
				tokenNumber = Integer.parseInt(request);
			}
			catch (NumberFormatException error)
			{
				System.out.println("Invalid token number!");
				return null;
			}
			
			if(!tokens.containsKey(tokenNumber))
			{
				System.out.format("There is no to token #%1$d!",tokenNumber);
				System.out.println();
				return null;
			}
			
			Token inner = tokens.get(tokenNumber);
			
			if(inner.type != TokenType.all && type != inner.type)
			{
				System.out.format("Token is of type %1$s", inner.type.name());
				System.out.println();
				return null;
			}
			
			if(!inner.allowDelegation && delegate)
			{
				System.out.println("This token cannot be delegated.");
				return null;
			}
			
			Date expiration = new Date();
			expiration.setTime(expiration.getTime() + SHORT_TOKEN_WINDOW);
			
			if(inner.expiration.before(expiration))
				expiration = inner.expiration;
			
			try {
				return new Token(
						"server",
						com.getName(),
						inner.owner,
						inner.file,
						type,
						allowDelegate, 
						expiration,
						com.getCertificate(),
						inner);
			} catch (IOException e) {
				return null;
			}
		}
	}
	
	private int getPort(String portStr)
	{
		int port;
		try
		{
			port = Integer.parseInt(portStr);
		}
		catch (NumberFormatException error)
		{
			port = -1;
		}
		
		if(port < 0 || port > MAX_PORT_NUMBER)
		{
			System.out.println("Invalid port number");
			port = -1;
		}
		return port;
	}
	
	private void buildCommandList()
	{
		Command[] temp = new Command[]
		{
			new Command("connect", 
					"<server url/ip address> [<server port>]",
					String.format("Starts a session with a file server. " +
							"If port is not set then the defalut port of %1$d will be used.", DEFAULT_SERVER_PORT))
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length < 2)
					{
						System.out.println("Insufficent arguments");
						return;
					}
					
					int port = DEFAULT_SERVER_PORT;
					if(args.length > 2)
					{
						port = getPort(args[2]);
						if(port < 0)
							return;
					}
					
					TransmitStatus status = com.startSession(args[1], port);
					printStatus(status);
				}
			},
			new Command("get",
					"(<server file number>|-t:<tokenNumber>) <local file>",
					"Sends a file to the files server.")
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length < 3)
					{
						System.out.println("Not enough argument!");
						printHelp();
						return;
					}
					
					Token token = createToken(args[1], TokenType.get, false, false);
					if(token == null)
						return;
					
					try {
						Util.signToken(token, com.getPrivateKey());
					} catch (SignatureException e) {
						System.out.println("Error signing token.");
						return;
					}
					
					File file = new File(args[2]);
					
					printStatus(com.getFile(token, file));
				}
			},
			new Command("put",
					"(<server file number>|-t:<tokenNumber>) <local file>",
					"Fetches a file from ther server and saves it to the local file.")
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length < 3)
					{
						System.out.println("Not enough argument!");
						printHelp();
						return;
					}
					
					Token token = createToken(args[1], TokenType.put, false, false);
					if(token == null)
						return;
					try
					{
						Util.signToken(token, com.getPrivateKey());
					} catch(SignatureException e){
						System.out.println("Error siging token");
						return;
					}
					File file = new File(args[2]);
					
					printStatus(com.putFile(token, file));
				}
			},
			new Command("getcert",
					"<certificate name> <server name> [server port]",
					"Requests a certificate from the CA")
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length < 3)
					{
						System.out.print("Not enough arguments!");
						printHelp();
						return;
					}
					int port = DEFAULT_CA_PORT;
					if(args.length > 3)
						port = getPort(args[3]);
					if(port < 0)
					{
						System.out.println("Invalid port number.");
						return;
					}
					printStatus(com.requestCertificate(args[1], args[2], port));
				}
			},
			new Command("delegate",
					"(<server file number>|-t:<tokenNumber>) <delegate name> <delegate url/ip> [type:(put|get|all)] [port:<delgate port>] [ttl:<time to live in seconds>]",
					String.format("Sends a ticket to another machine giving them permition to open a file to which you currently have rights. " +
					"If port is not set then the defalut port of %1$d will be used.", DEFAULT_CLIENT_PORT))
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length < 4)
					{
						System.out.println("Not enough argument!");
						printHelp();
						return;
					}
					
					Token token = createToken(args[1], TokenType.all, false, false);
					if(token == null)
						return;
					token.deleg = args[2];
					try {
						Util.signToken(token, com.getPrivateKey());
					} catch (SignatureException e) {
						System.out.println("Error signing token");
						return;
					}
					
					Date defaultWindow = new Date();
					defaultWindow.setTime(defaultWindow.getTime() + DEFAULT_DELEGATE_TOKEN_WINDOW);
					token.expiration = defaultWindow;
					
					int port = DEFAULT_CLIENT_PORT;
					
					for(int i = 4; i < args.length; i++)
					{
						String[] arg = args[i].split(":",2);
						
						if(arg.length != 2)
						{
							System.out.println("Invalid Argument.");
							return;
						}
						if(arg[0].equals("port"))
						{
							port = getPort(arg[1]);
							if(port < 0)
							{
								System.out.println("Invalid Port Number");
								return;
							}
						}
						else if(arg[0].equals("type"))
						{
							try
							{
								token.type = TokenType.valueOf(arg[1]);
							}
							catch(Exception e)
							{
								System.out.println("Invalid token type.");
								return;
							}
						}
						else if(arg[0].equals("ttl"))
						{
							try
							{
								long seconds = Long.parseLong(arg[1]);
								Date expiration = new Date();
								expiration.setTime(expiration.getTime() + seconds * 1000);
								token.expiration = expiration;
							}
							catch(Exception e)
							{
								System.out.println("Invalid time to live.");
								return;
							}
						}
						else
						{
							System.out.println("Unknown argument.");
						}
					}
					try
					{
						Util.signToken(token, com.getPrivateKey());
					}
					catch(SignatureException e)
					{
						System.out.println("Error signing token.");
						return;
					}
					
					printStatus(com.SendToken(token, args[3], port));
				}
			},
			new Command("gettokens",
					"[<listening port>]",
					"Opens up a secure port for listening. A certificate is required before this can be done.")
			{
				public void action(String[] args, TransmitionBox com)
				{
					int port = Client.DEFAULT_CLIENT_PORT;
					if(args.length > 1)
					{
						port = getPort(args[1]);
					}
					if(port < 0)
						return;
					
					try
					{
						if (!com.waitConnection(port, new ReceiveTokenHandle(com)))
						{
							System.out.println("Unable to open listener. One is already open or there is no certificate.");
						}
					}
					catch(IOException error)
					{
						System.out.format("Unable to listen on port %1$d!", port);
						System.out.println();
						return;
					}
				}
			},
			new Command("tokens",
					null, 
					"lists all tokes received with token number, ")
			{
				public void action(String[] args, TransmitionBox com)
				{
					for(int tokenNum: tokens.keySet())
					{
						Token token = tokens.get(tokenNum);
						Date now = new Date();
						
						long dif = (token.expiration.getTime() -  now.getTime())/1000;
						
						String time = "<expired>";
						
						if(dif > 0)
						{
							int sec = (int)(dif % 60);
							dif /= 60;
							int min = (int)(dif % 60);
							dif /= 60;
							int hour = (int)(dif % 60);
							int day = (int)(dif / 24);
							
							time = String.format("%1$03d %2$02d:%3$02d:%4$02d", day, hour, min, sec);
						}
						
						System.out.format("%1$-3d file:%2$-8d owner:%3$-8s type:%4$-3s ttl:%5$s", tokenNum, token.file, token.owner, token.type.name(), time);
						System.out.println();
					}
				}
			},
			new Command("exit",
					null,
					"closes the session and ends the process")
			{
				public void action(String[] args, TransmitionBox com)
				{
					running = false;
					com.endSession();
					com.stop();
				}
			},
			new Command("close",
					null,
					"closes the session")
			{
				public void action(String[] args, TransmitionBox com)
				{
					
				}
			},
			new Command("help",
					"[command]",
					"lists all commands or gives a discriptions of the command passed as an argument")
			{
				public void action(String[] args, TransmitionBox com)
				{
					if(args.length <= 1)
					{
						for(Command command:commands.values())
						{
							System.out.println(command.getName());
						}
						System.out.println("use: help <command> to learn more about a specific command.");
					}
					else
					{
						Command command = commands.get(args[1]);
						if(command == null)
						{
							System.out.format("Command '%1$s' is not a recognized command.%2$s", args[1], newLine);
							return;
						}
						
						command.printHelp();
					}
				}
			}
		};
		for(Command command:temp)
		{
			commands.put(command.getName(), command);
		}
	}
	
	public void run(String[] args)
	{
		try
		{	
			//commands.get("getcert").action(new String[] {"getcert", "allen", "localhost"}, com);
			//commands.get("connect").action(new String[] {"connect", "localhost"}, com);
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			PrintStream out = System.out;
			while(running)
			{
				out.println();
				out.format("%1$s>", com.getName());
				String line = in.readLine().toLowerCase();
				String[] commandArgs = line.split("\\s");
				if(commandArgs.length == 1 && commandArgs[0].isEmpty())
					continue;
				if(commands.containsKey(commandArgs[0]))
					commands.get(commandArgs[0]).action(commandArgs, com);
				else
				{
					out.format("Unknown command: %1$s%2$s", commandArgs[0], newLine);
				}
			}
		}
		catch(IOException error)
		{
			running = false;
		}
	}
}
